La capa del modelo en una aplicación orientada a objetos es la que contiene la lógica de negocio, esto es la estructura de los datos con los que se trabaja y todos los cálculos, validaciones y procesos asociados a esos datos.
OpenXava es un marco orientado al modelo, en donde el modelo es lo más importante, y todo lo demás (p. ej. la interfaz gráfica) depende de él.
La forma de definir el modelo en OpenXava es mediante simples clases Java (aunque también existe una versión XML) y un poquito de Java. OpenXava provee una aplicación completamente funcional a partir de la definición del modelo.
Componente de negocio
La unidad básica para crear aplicaciones OpenXava es el componente de negocio. Un componente de negocio se define usando una clase Java llamada Entity. Esta clase es una entidad EJB3 convencional, o con otras palabras, un POJO con anotaciones que sigue el estándar Java Persistence API (JPA).
JPA es el estándar de Java para la persistencia, es decir, para objetos que guardan su estado en una base de datos. Si sabes desarrollar usando POJOs con JPA, ya sabes como desarrollar aplicaciones OpenXava.
Usando una simple clase Java podemos definir un Componente de Negocio con:
Modelo: Estrutura de datos, validaciones, calculos, etc.
Vista: Cómo se puede mostrar el modelo al usuario.
Datos tabulares: Cómo se muestra los datos de este componentes en modo lista (en formato tabular).
Mapeo objeto/relacional: Cómo grabar y leer el estado de los objetos desde la base de datos.
Este capítulo explica cómo definir la parte del modelo, es decir, todo sobre la estructura, las validaciones, los cálculos, etc.
Entidad
Para definir la parte del modelo hemos de crear una clase Java o Groovy(desde v4m6) con anotaciones. Además de sus propias anotaciones, OpenXava sporta anotaciones de JPA, Hibernate Validator e Hibernate Annotations. Esta clase Java es una entidad, es decir, una clase persistente que representa concepto de negocio.
En este capítulo JPA se usa para indicar que es una anotación estándar de Java Persistent API, HV para indicar que es una anotación de Hibernate Validator, HA para indicar que es una anotación de Hibernate Annotations y OX para indicar que es una anotación de OpenXava.
Ésta es la sintáxis para una entidad:
@Entity (JPA, uno, obligado): Indica que esta clase es una entidad JPA, con otras palabras, sus instancias serán objetos persistentes.
@EntityValidator (OX, varios, opcional): Ejecuta una validación a nivel de modelo. Este validador puede recibir el valor de varias propiedades del modelo. Para validar una sola propiedad es preferible poner el validador a nivel de propiedad.
@RemoveValidator (OX, varios, opcional): Se ejecuta antes de borrar, y tiene la posibilidad de vetar el borrado del objeto.
Declaración de la clase: Como en un clase de Java convencional. Podemos usar extends e implements.
Propiedades: Propiedades de Java convencionales. Representan el estado principal del objeto.
Referencias: Referencias a otras entidades o clases incrustables.
Colecciones: Colecciones de referencias a otras entidades o clases incrustables.
Buscadores: Los buscadores son métodos estáticos que hacen búsquedas usando las prestaciones de consulta de JPA.
Métodos de retrollamada: Los métodos JPA de retrollamada (callbacks) para insertar lógica al crear, modificar, cargar, borrar, etc
Clases incrustables (Embeddable)
Tal y como indica la especificación JPA: "Una entidad puede usar otras clases finamente granuladas para representar su estado. Instancias de estas clases, no como en el caso de las entidades, no tiene identidad persistente. En vez de eso, existen solo como objetos incrustados de una entidad a la que pertenecen. Estos objetos incrustados son propiedad exclusiva de sus entidades dueñas, y no se comparten entre entidades persistentes."
La sintaxis para una clase incrustada es:
@Embeddable (JPA, una, requerido): Indica que esta clase es una clase incrustada de JPA, en otras palabras, sus instancias serán parte de objetos persistente.
Declaración de la clase: Como una clase Java convencional. Podemos uar extends y implements.
Una propiedad representa parte del estado de un objeto que se puede consultar y en algunos casos cambiar. El objeto no tiene la obligación de guardar físicamente la información de la propiedad, solo de devolverla cuando se le pregunte.
La sintaxis para definir una propiedad es:
@Stereotype // 1
@Column(length=) @Column(precision=) @Max @Length(max=) @Digits(integer=)// 2
@Digits(integer=) @Digits(fraction=)// 3
@Required @Min @Range(min=) @Length(min=)// 4
@Id // 5
@Hidden // 6
@SearchKey // 7
@Version // 8
@Formula // 9 Nuevo en v3.1.4
@Calculation // 10 Nuevo en v5.7
@DefaultValueCalculator // 11
@PropertyValidator // 12private tipo nombrePropiedad;// 13public tipo getNombrePropiedad(){ ... }// 13publicvoid setNombrePropiedad(tipo nuevoValor){ ... }// 13
@Stereotype (OX, opcional): Permite especificar un comportamiento especial para cierta propiedades.
@Column(length=) (JPA), @Max (BV), @Length(max=) (HV), @Digits(integer=) (BV): opcional, normalmente solo se usa una): Longitud en caracteres de la propiedad, excepto para @Max que es el valor máximo. Especialmente útil a la hora de generar interfaces gráficas. Si no especificamos longitud asume un valor por defecto asociado al tipo o estereotipo que se obtiene de default-size.xml o longitud-defecto.xml. @Max y @Digits son de Bean Validation (nuevo en v4.1) o de Hibernate Validator (hasta v5.2.x).
@Column(scale=) (JPA), @Digits(fraction=) (BV): Escala (tamaño de la parte decimal) de la propiedad. Sólo aplica a propiedades numéricas. Si no se usa @Column o @Digits se asume un valor por defecto, si se usa @Column sin scale se asume un valor por defecto, sin embargo si se usa @Digits sin fraction se asume 0. El valor por defecto asociado al tipo o estereotipo que se obtiene de default-size.xml o longitud-defecto.xml. @Digits es de Bean Validation (new in v4.1) o de Hibernate Validator (hasta v5.2.x).
@Required (OX), @Min (BV), @Range(min=) (HV), @Length(min=) (HV) (opcional, normalmente solo se usa una): Indica si esa propiedad es requerida. En el caso de @Min, @Range y @Length tenemos que poner un valor mayor que cero para min para que se asuma la propiedad como requerida. Por defecto es true para las propiedades clave ocultas (nuevo en v2.1.3) y false en todos los demás casos. Al grabar OpenXava comprobará si las propiedades requeridas están presentes, si no lo están no se producirá la grabación y se devolverá una lista de errores de validación. La lógica para determinar si una propiedad está presente o no se puede configurar creando un archivo validators.xml o validadores.xml en nuestro proyecto. Podemos ver la sintaxis en OpenXava/xava/default-validators.xml. @Required y @Min son restricciones de Bean Validation desde v5.3 y eran restricciones de Hibernate Validator hasta v5.2.x.
@Id (JPA, opcional): Para indicar si una propiedad forma parte de la clave. Al menos una propiedad (o referencia) ha de ser clave. La combinación de propiedades (y referencias) clave se debe mapear a un conjunto de campos en la base de datos que no tengan valores repetidos, típicamente con la clave primaria.
@Hidden (OX, opcional): Una propiedad oculta es aquella que tiene sentido para el desarrollador pero no para el usuario. Las propiedades ocultas se excluyen cuando se generan interfaces gráficas automáticas, sin embargo a nivel de código generado están presentes y son totalmente funcionales, incluso si se les hace alusión explicita podrían aparecer en una interfaz gráfica.
@SearchKey (OX, optional): Las propiedades clave de búsqueda se usan por los usuarios para buscar los objetos. Son editables en la interfaz de usuario de las referencias permitiendo al usuario teclear su valor para buscar. OpenXava usa las propiedades clave (@Id) para buscar por defecto, y si la propiedades clave (@Id) están ocultas usa la primera propiedad en la vista. Con @SearchKey podemos elegir las propiedades para buscar explicitamente.
@Version (JPA, opcional): Una propiedad versión se usa para el control de concurrencia optimista. Si queremos control de concurrencia solo necesitamos tener una propiedad marcada como @Version en nuestra entidad. Solo podemos especificar una propiedad de versión por entidad. Los siguientes tipos son soportados para propiedades versión: int, Integer, short, Short, long, Long, Timestamp. Las propiedades de versión también se consideran ocultas.
@Formula (HA, opcional): (Nuevo en v3.1.4) Para calcular el valor de la propiedad usando la base de datos. Tiene que ser un fragmento válido de SQL.
@Calculation (OX, uno, opcional): (Nuevo en v5.7) Expresión aritmética para calcular el valor de la propiedad. El cálculo se hace en la interfaz de usuario cuando alguno de los operandos cambia.
@DefaultValueCalculator (OX, uno, optional): Para implementar la lógica para calcular el valor inicial de la propiedad. Una propiedad con @DefaultValueCalculator sí tiene setter y es persistente.
@PropertyValidator (OX, varios, opcional): Indica la lógica de validación a ejecutar sobre el valor a asignar a esta propiedad antes de crear o modificar.
Declaración de la propiedad: Una declaración de propiedad Java normal y corriente con getters y setters. Podemos crear una propiedad calculada usando solo un getter sin campo ni setter. Cualquier tipo legal para JPA está permitido, solo hemos de proveer un Hibernate Type para grabar en la base de datos y un editor OpenXava para dibujar como HTML.
Estereotipo
Un estereotipo (@Stereotype) es la forma de determinar un comportamiento especifico dentro de un tipo. Por ejemplo, un nombre, un comentario, una descripción, etc. todos corresponden al tipo Java java.lang.String pero si queremos que los validadores, logitud por defecto, editores visuales, etc. sean diferente en cada caso y necesitamos afinar más; lo podemos hacer asignando un esterotipo a cada uno de estos casos. Es decir, podemos tener los estereotipos NOMBRE, TEXTO_GRANDE o DESCRIPCION y asignarlos a nuestras propiedades.
El OpenXava viene configurado con los siguientes estereotipos:
DISCUSION, DISCUSSION (nuevo en v5.6) (instrucciones)
Vamos a ver como definiríamos un estereotipo propio. Crearemos uno llamado NOMBRE_PERSONA para representar nombres de persona.
Editamos (o creamos) el archivo editors.xml o editores.xml en nuestra carpeta xava. Y añadimos
De esta forma indicamos que editor se ha de ejecutar para editar y visualizar propiedades con el estereotipo NOMBRE_PERSONA.
Además es útil indicar la longitud por defecto, eso se hace editando default-size.xml o longitud-defecto.xml:
Y así si no ponemos longitud asumirá 40 por defecto.
Menos común es querer cambiar el validador para requerido, pero si queremos cambiarlo lo podemos hacer añadiendo a validators.xml o validadores.xml de nuestro proyecto lo siguiente:
En este caso asume 40 longitud y tipo String, así como ejecutar el validador NotBlankCharacterValidator
para comprobar que es requerido.
Estereotipo GALERIA_IMAGENES
Si queremos que una propiedad de nuestro componente almacene una galería de imágenes. Solo necesitamos declarar que nuestra propiedad sea del estereotipo GALERIA_IMAGENES. De esta manera:
Además, en el mapeo tenemos que mapear la propiedad contra una columna adecuada para almacenar una cadena (String) con 32 caracteres de longitud (VARCHAR(32)).
Y ya está todo.
Pero, para que nuestra aplicación soporte este estereotipo necesitamos configurar nuestro sistema.
Lo primero es crear a tabla en la base de datos para almacenar las imágenes:
CREATETABLE IMAGENES (
ID VARCHAR(32)NOTNULLPRIMARYKEY,
GALLERY VARCHAR(32)NOTNULL,
IMAGE BLOB);
CREATEINDEX IMAGENES01
ON IMAGENES (GALLERY);
El tipo de la columna IMAGE puede ser un tipo más adecuado para almacenar byte [] en el caso de nuestra base de datos (por ejemplo LONGVARBINARY) .
Y finalmente necesitamos definir el mapeo en nuestro archivo persistence/hibernate.cfg.xml, así:
Después de todo esto ya podemos usar el estereotipo GALERIA_IMAGENES en los componentes de nuestra aplicación.
Estereotipos ARCHIVO (nuevo en v5.0) y ARCHIVOS (nuevo en v5.1)
Si queremos que una propiedad de nuestro componente adjunte un archivo, sólo necesitamos declarar una propiedad con el estereotipo ARCHIVO. De esta manera:
OpenXava puede almacenar los archivos en base de datos o en el sistema de archivos.
Almacenamiento en el Sistema de Archivos
Éste es por defecto y no requiere de configuraciones adicionales.
El directorio de almacenamiento es $HOME/oxfiles (GNU/Linux) o %USERPROFILE%\oxfiles (Windows). Podemos configurar un directorio diferente mediante la propiedad filesPath en xava.properties.
Almacenamiento en Base de Datos
Debemos realizar tres configuraciones adicionales:
Configurar en xava.properties de nuestro proyecto la propiedad filePersistorClass.
Observa que hemos añadido <class>org.openxava.web.editors.AttachedFile</class> a ambas unidades de persistencia.
Al generar nuestra base de datos se creará la tabla OXFILES:
CREATETABLE OXFILES (
ID VARCHAR(32)NOTNULLPRIMARYKEY,
NAME VARCHAR(255),DATA LONGVARBINARY,
LIBRARYID VARCHAR(32));
Debemos verificar que el tipo de la columna DATA sea el tipo más adecuado para almacenar byte[] (en nuestro caso LONGVARBINARY).
Estereotipo DISCUSION (nuevo en v5.6)
Si quieres que una propiedad de tu componente contenga un hilo de discusión, como los que hay en los foros, gestores de incidencias, blogs, etc. Sólo has de declarar tu propiedad con el estereotipo DISCUSION, de esta manera:
Si además quieres que cuando se borre tu entidad su discusión también se elimine completamente de la base de datos, has de añadir el siguiente código a tu entidad:
Verifica que persistence.xml contiene la entidad DiscussionComment, si no añádelo:
<persistence-unitname="default"><provider>org.hibernate.ejb.HibernatePersistence</provider><non-jta-data-source>java:comp/env/jdbc/OpenXavaTestDS</non-jta-data-source><class>org.openxava.session.GalleryImage</class><class>org.openxava.web.editors.DiscussionComment</class><!-- AÑADE ESTA LÍNEA -->
...
</persistence-unit>
Fíjate que hemos añadido <class>org.openxava.web.editors.DiscussionComment</class>. Cuando se genere la base datos, la tabla OXDISCUSSIONCOMMENTS se creará:
CREATETABLE OXDISCUSSIONCOMMENTS (
ID VARCHAR(32)NOTNULL,
COMMENT CLOB(16777216),
DISCUSSIONID VARCHAR(32),TIMETIMESTAMP,
USERNAME VARCHAR(30),PRIMARYKEY(ID));
CREATEINDEX OXDISCUSSIONCOMMENTS_DISCUSSIONID_INDEX
ON OXDISCUSSIONCOMMENTS (DISCUSSIONID);
Comprueba que el tipo para la columna COMMENT es el más adecuado para almacenar un texto grande (CLOB por defecto) en tu base de datos, si no haz un ALTER COLUMN para poner un tipo mejor.
Con Tomcat 6 has de editar la clase org.openxava.web.editors.DiscussionComment y quitarle la anotación @Lob de la propiedad comment.
Concurrencia y propiedad versión
Concurrencia es la habilidad de una aplicación para permitir que varios usuarios graben datos al mismo tiempo sin perder información. OpenXava usa un esquema de concurrencia optimista. Usando concurrencia optimista los registros no se bloquean permitiendo un alto nivel de concurrencia sin perder la integridad de la información.
Por ejemplo, si un usuario A lee un registro y entonces un usuario B lee el mismo registro, lo modifica y graba los cambios, cuando el usuario A intente grabar el registro recibirá un error y tendrá que refrescar los datos y reintentar su modificación.
Para activar el soporte de concurrencia para un componente OpenXava solo necesitamos declarar una propiedad usando @Version, de esta manera:
Esta propiedad es para uso del mecanismo de persistencia (Hibernate o JPA), ni nuestra aplicación ni usuarios deberían acceder directamente a ella.
Enums
OpenXava soporta enums de Java 5. Un enum permite definir una propiedad que solo puede contener los valores indicados.
Es fácil de usar, veamos un ejemplo:
private Distancia distancia;publicenum Distancia { LOCAL, NACIONAL, INTERNACIONAL };
La propiedad distancia solo puede valer LOCAL, NACIONAL o INTERNACIONAL, y como no hemos puesto @Required también permite valor vacío (null). Desde v5.3 si pones @Required, la primera opción es por defecto y ya no mostrará valor vacío. Si deseas cambiar la opción por defecto usa @DefaultValueCalculator. Desde v5.6.1 los enums anotados con @Required en una clase incrustable mostrarán valor vacío si ésta es utilizada en una colección de elementos.
A nivel de interfaz gráfico la implementación web actual usa un combo. La etiqueta para cada valor se obtienen de los archivos i18n.
A nivel de base datos por defecto guarda el entero (0 para LOCAL, 1 para NACIONAL, 2 para INTERNACIONAL y null para cuando no hay valor), pero esto se puede configurar fácilmente para poder usar sin problemas bases de datos legadas. Ver más de esto último en el capítulo sobre mapeo.
Propiedades calculadas
Las propiedades calculadas son de solo lectura (solo tienen getter) y no persistentes (no se almacenan en ninguna columna de la tabla de base de datos).
Una propiedad calculada se define de esta manera:
Y resultado contendrá 332,772.
Cuando la propiedad precioUnitarioEnPesetas se visualiza al usuario no es editable, y su editor tiene una longitud de 10, indicado usando @Max(9999999999L) (2). También, dado que usamos @Depends("precioUnitario") (1) cuando el usuario cambie la propiedad precioUnitario en la interfaz de usuario la propiedad precioUnitarioEnPesetas será recalculada y su valor será refrescado de cara al usuario.
Desde una propiedad calculada tenemos acceso a conexiones JDBC. Un ejemplo:
@Max(999)publicint getCantidadLineas(){// Un ejemplo de uso de JDBCConnection con = null;try{
con = DataSourceConnectionProvider.getByComponent("Factura").getConnection();// 1String tabla = MetaModel.get("LineaFactura").getMapping().getTable();PreparedStatement ps = con.prepareStatement("select count(*) from " + tabla +
" where FACTURA_AÑO = ? and FACTURA_NUMERO = ?");
ps.setInt(1, getAño());
ps.setInt(2, getNumero());ResultSet rs = ps.executeQuery();
rs.next();Integer result = newInteger(rs.getInt(1));
ps.close();return result;}catch(Exception ex){
log.error("Problemas al calcular cantidad de líneas de una Factura", ex);// Podemos lanzar cualquier RuntimeException aquíthrownewSystemException(ex);}finally{try{
con.close();}catch(Exception ex){}}}
Es verdad, el código JDBC es feo y complicado, pero a veces puede ayudar a resolver problemas de rendimiento. La clase DataSourceConnectionProvider nos permite obtener la conexión asociada a la misma fuente de datos que la entidad indicada (en este caso Factura). Esta clase es para nuestra conveniencia, también podemos acceder a una conexión JDBC usando JNDI o cualquier otro medio que queramos. De hecho, en una propiedad calculada podemos escribir cualquier código que Java nos permita.
Si estamos usando acceso basado en propiedades, es decir si anotamos los getters o setters, entonces hemos de añadir la anotación @Transient a nuestra propiedad calculada, de esta forma:
privatelong codigo;
@Id @Column(length=10)// Anotamos el getter,publiclong getCodigo(){// por tanto JPA usará acceso basado en propiedades para nuestra clasereturn codigo;}publicvoid setCodigo(long codigo){this.codigo = codigo;}
@Transient // Hemos de anotar como Transient nuestra propiedad calculadapublicString getZoneOne(){// porque usamos acceso basado en propiedadesreturn"En ZONA 1";}
Formula (nuevo en v3.1.4)
Usando @Formula de Hibernate Annotations podemos definir un cálculo para nuestra propiedad. Este cálculo se expresa usando SQL, y es ejecutado en la propia base de datos, en vez de por Java. Simplemente hemos de escribir un fragmento válido de SQL:
El uso es simple. Hemos de poner el cálculo como lo hariamos si lo tuvieramos que poner en una sentencia SQL.
Normalmente las propiedades con @Formula son propiedades de solo lectura, es decir, solo tienen getter, no tienen setter. Cuando el objeto es leído de la base de datos se hace el cálculo por la misma base de datos y se rellena la propiedad con el resultado.
Esto es una alternativa a las propiedades calculadas. Tiene la ventaja de que el usuario puede filtrar por esta propiedad en modo lista, y la desventaja de que hemos de usar SQL en vez de Java, y no podemos usar @Depends para recalcular el valor en vivo.
Calculador valor por defecto
Con @DefaultValueCalculator podemos asociar lógica a una propiedad, en este caso la propiedad es lectura y escritura. Este calculador se usa para calcular el valor inicial. Por ejemplo:
En este caso cuando el usuario intenta crear una nueva factura (por ejemplo) se encontrará con que el campo de año ya tiene valor, que él puede cambiar si quiere. La lógica para generar este valor está en la clase CurrentYearCalculator class, así:
packageorg.openxava.calculators;importjava.util.*;/**
* @author Javier Paniza
*/publicclass CurrentYearCalculator implements ICalculator {publicObject calculate()throwsException{Calendar cal = Calendar.getInstance();
cal.setTime(new java.util.Date());returnnewInteger(cal.get(Calendar.YEAR));}}
Es posible personalizar el comportamiento de un calculador poniendo el valor de sus propiedades, como sigue:
En este caso para calcular el valor por defecto OpenXava instancia StringCalculator y entonces inyecta el valor "BUENA" en la propiedad string de StringCalculator, y finalmente llama al método calculate() para obtener el valor por defecto para relacionConComercial. Como se ve, el uso de la anotación @PropertyValue permite crear calculadores reutilizable. @PropertyValue permite inyectar valores desde otras propiedades visualizadas, de esta forma:
En este caso antes de ejecutar el calculador OpenXava llena la propiedad permisoConducir de CalculadorObservacionesTransportista con el valor de la propiedad visualizada tipo de la referencia permisoConducir. Como se ve el atributo from soporta propiedades calificadas (referencia.propiedad). Además, cada ve que permisoConducir.tipo cambia observaciones se recalcula (nuevo en v5.1, con versiones anteriores se recalculaba solo la primera vez).
Además podemos usar @PropertyValue sin from ni value:
En este caso OpenXava coge el valor de la propiedad visualizada codigoFamilia y lo inyecta en la propiedad codigoFamilia del calculador, es decir @PropertyValue(name="codigoFamilia") equivale a @PropertyValue(name="codigoFamilia", from="codigoFamilia").
Desde un calculador tenemos acceso a conexiones JDBC, he aquí un ejemplo:
Para usar JDBC nuestro calculador tiene que implementar IJDBCCalculator (1) y entonces recibirá un IConnectionProvider (2) que podemos usar dentro de calculate().
OpenXava dispone de un conjunto de calculadores incluidos de uso genérico, que se pueden encontrar en org.openxava.calculators.
Valores por defecto al crear
Podemos indicar que el valor sea calculado justo antes de crear (insertar en la base de datos) un objeto por primera vez.
Usualmente para las claves usamos el estándar JPA. Por ejemplo, si queremos usar una columna identity (auto incremento) como clave:
Ver la sección 9.1.9 de la especificación JPA 1.0 (parte de JSR-220) para aprender más sobre @GeneratedValues.
Si queremos usar nuestra propia lógica para generar el valor al crear, o bien queremos generar un nuevo valor para propiedades que no son clave entonces no podemos usar el @GeneratedValue de JPA, aunque es fácil resolver estos casos con JPA. Solo necesitamos añadir este código a nuestra clase:
La anotación JPA @PrePersist hace que este método se ejecute antes de insertar datos por primera vez en la base de datos, en este método podemos calcular el valor para nuestra clave o incluso para propiedades no clave con nuestra propia lógica.
Validador de propiedad
Un validador de propiedad (@PropertyValidator) ejecuta la lógica de validación sobre el valor que se vaya a asignar a esa propiedad antes de grabar. Una propiedad puede tener varios validadores:
La forma de configurar el validador (con los @PropertyValue, aunque el atributo from no funciona, hay que usar value siempre) es exactamente igual como en los calculadores. Con el atributo onlyOnCreate=”true” se puede definir que esa validación solo se ejecute cuando se crea el objeto, y no cuando se modifica.
El código del validador es:
Un validador ha de implementar IPropertyValidator (1), esto le obliga a tener un método validate() en donde se ejecuta la validación de la propiedad. Los argumentos del método validate() son:
Messages errores: Un objeto de tipo Messages que representa un conjunto de mensajes (una especie de colección inteligente) y es donde podemos añadir los problemas de validación que encontremos.
Object valor: El valor a validar.
String nombreObjeto: Nombre del objeto al que pertenece la propiedad a validar. Útil para usarlo en los mensajes de error.
String nombrePropiedad: Nombre de la propiedad a validar. Útil para usarlo en los mensajes de error.
Como se ve cuando encontramos un error de validación solo tenemos que añadirlo (con errores.add()) enviando un identificador de mensaje y los argumentos. Para que este validador produzca un mensaje significativo tenemos que tener en nuestro archivo de mensajes i18n la siguiente entrada:
excluir_cadena={0} no puede contener {2} en {1}
Si el identificador que se envía no está en el archivo de mensajes, sale tal cual al usuario; pero lo recomendado es siempre usar identificadores del archivo de mensajes.
La validación es satisfactoria si no se añaden mensajes y se supone fallida si se añaden. El sistema recolecta todos los mensajes de todos los validadores antes de grabar y si encuentra los visualiza al usuario y no graba.
A partir de v4.6.1 también es posible usar en el validador el mensaje de @PropertyValidator. Es decir, podemos escribir:
Si el mensaje está entre llaves se obtiene de los archivos i18n, si no se usa tal cual.
Además, hemos de implementar la interfaz IWithMessage en el validador:
publicclass ValidadorTituloLibro implements IPropertyValidator, IWithMessage {privateString message;publicvoid setMessage(String message)throwsException{this.message = message;// Este es message de @PropertyValidator}publicvoid validate(Messages errors, Object value, String propertyName, String modelName){if(((String)value).contains("RPG")){
errors.add(message);// Podemos añadir el mensaje directamente}}}
El mensaje especificado en la anotación @PropertyValidator, libro_rpg_no_permitido, se inyecta en el validador llamando a setMessage(). Este mensaje puede ser añadido directamente como un error.
El paquete org.openxava.validators contiene algunos validadores de uso común. @PropertyValidator está definida como una restriccion de Bean Validation a partir de v5.3 y como una restricción de Hibernate Validator hasta v5.2.x.
Si necesitas usar JPA en tu validador, mira Usar JPA en un validador o método de retrollamada.
Validador por defecto (nuevo en v2.0.3)
Podemos definir validadores por defecto para las propiedades de cierto tipo o estereotipo. Para esto se usa el archivo xava/validadores.xml de nuestro proyecto para definir en él los validadores por defecto.
Por ejemplo, podemos definir en nuestro xava/validadores.xml lo siguiente:
Esta propiedad será validada usando ValidadorNombrePersona aunque la propiedad misma no defina ningun validador. ValidadorNombrePersona se aplica a todas las propiedades con el estereotipo NOMBRE_PERSONA.
Podemos también asignar validadores por defecto a un tipo.
En el archivo validadores.xml podemos definir también los validadores para determinar si un valor requerido está presente (ejecutado cuando usamos @Required). Además podemos asignar nombre (alias) a las clases de los validadores.
Podemos aprender más sobre los validadores examinando OpenXava/xava/default-validators.xml y OpenXavaTest/xava/validators.xml.
Los validadores por defecto no se aplican cuando grabamos nuestras entidades directamente con la api de JPA.
Cálculo (nuevo en v5.7)
Con @Calculation podemos definir una expresión aritmética para hacer el cálculo de la propiedad. La expresión puede contener +, -, *, /, (), valores numéricos y nombres de propiedades de la misma entidad. Por ejemplo:
Fíjate como trabajador.precioHora se usa para obtener el valor de una referencia.
El cálculo se ejecuta y visualiza cuando el usuario cambia cualquier valor de las propiedades usadas en la expresión en la interfaz de usuario, sin embargo el valor no se graba hasta que el usuario no pulsa en el botón de grabar.
Referencias
Una referencia hace que desde una entidad o agregado se pueda acceder otra entidad o agregado. Una referencia se traduce a código Java como una propiedad (con su getter y su setter) cuyo tipo es el del modelo al que se referencia. Por ejemplo un Cliente puede tener una referencia a su Comercial, y así podemos escribir código Java como éste:
para acceder al nombre del comercial de ese cliente.
La sintaxis para definir referencias es:
@Required // 1
@Id // 2
@SearchKey // 3 Nuevo en v3.0.2
@DefaultValueCalculator // 4
@ManyToOne(// 5
optional=false// 1)private tipo nombreReferencia;// 5public tipo getNombreReferencia(){ ... }// 5publicvoid setNombreReferencia(tipo nuevoValor){ ... }// 5
@ManyToOne(optional=false) (JPA), @Required (OX) (opcional, el JPA es el preferido): Indica si la referencia es requerida. Al grabar OpenXava comprobará si las referencias requeridas están presentes, si no lo están no se producirá la grabación y se devolverá una lista de errores de validación.
@Id (JPA, opcional): Para indicar si la referencia forma parte de la clave. La combinación de propiedades y referencias clave se debe mapear a un conjunto de campos en la base de datos que no tengan valores repetidos, típicamente con la clave primaria.
@DefaultValueCalculator (OX, one, opcional): Para implementar la lógica para calcular el valor inicial de la referencia. Este calculador ha de devolver el valor de la clave, que puede ser un dato simple (solo si la clave del objeto referenciado es simple) o un objeto clave (un objeto especial que envuelve la clave primaria).
@SearchKey (OX, optional): (Nuevo en v3.0.2) Las referencias clave de búsqueda se usan por los usuarios para buscar los objetos. Son editables en la interfaz de usuario de las referencias permitiendo al usuario teclear su valor para buscar. OpenXava usa los miembros clave (@Id) para buscar por defecto, y si los miembros clave (@Id) están ocultos usa la primera propiedad en la vista. Con @SearchKey podemos elegir referencias para buscar explícitamente.
Declaración de la referencia: Una declaración de referencia convencional de Java con sus getters y setters. La referencia se marca con @ManyToOne (JPA) y el tipo ha de ser otra entidad.
Una referencia llamada comercial a la entidad Comercial.
Una referencia llamada comercialAlternativo a la entidad Comercial. En este caso usamos fetch=FetchType.LAZY, de esta manera los datos son leidos de la base de datos bajo demanda. Este es el enfoque más eficiente, pero no es el valor por defecto en JPA, por tanto es aconsejable usar siempre fetch=FetchType.LAZY al declarar las referencias.
Si asumimos que esto está en una entidad llamada Cliente, podemos escribir:
En una referencia @DefaultValueCalculator funciona como en una propiedad, solo que hay que devolver el valor de la clave de la referencia.
Por ejemplo, en el caso de una referencia con clave simple podemos poner:
@ManyToOne(optional=false, fetch=FetchType.LAZY) @JoinColumn(name="FAMILY")
@DefaultValueCalculator(value=IntegerCalculator.class, properties=
@PropertyValue(name="value", value="2"))private Familia familia;
Como se puede ver se devuelve un entero, es decir, el valor para familia por defecto es la familia cuyo código es el 2.
En el caso de clave compuesta sería así:
packageorg.openxava.test.calculadores;importorg.openxava.calculators.*;/**
* @author Javier Paniza
*/publicclass CalculadorDefectoAlmacen implements ICalculator {publicObject calculate()throwsException{
Almacen clave = new Almacen();
clave.setNumber(4);
clave.setZoneNumber(4);return clave;}}
Devuelve un objeto de tipo Almacen pero rellenando sólo las propiedades clave.
Usar referencias como clave
Podemos usar referencias como clave, o como parte de la clave. Hemos de declarar la referencia como @Id, y usar una clase clave, como sigue:
@Entity
@IdClass(DetalleAdicionalKey.class)publicclass DetalleAdicional {// JoinColumn se especifica también en DetalleAdicionalKey por un// bug de Hibernate, ver http://opensource.atlassian.com/projects/hibernate/browse/ANN-361
@Id @ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="SERVICIO")private Servicio servicio;
@Id @Hidden
privateint contador;
...
}
Necesitamos escribir la clase clave aunque la clave sea solo una referencia con una sola columna clave.
Es mejor usar esta característica sólo cuando estemos trabajando contra bases de datos legadas, si tenemos control sobre el esquema es mejor usar un id autogenerado.
Referencias incrustadas
Podemos referenciar a una clase incrustable usando la anotación @Embedded. Por ejemplo, en la entidad principal podemos escribir:
@Embedded
private Direccion direccion;
Y hemos de definir la clase Direccion como incrustable:
packageorg.openxava.test.model;importjavax.persistence.*;importorg.openxava.annotations.*;/**
*
* @author Javier Paniza
*/
@Embeddable
publicclass Direccion implements IConPoblacion {
@Required @Column(length=30)privateString calle;
@Required @Column(length=5)privateint codigoPostal;
@Required @Column(length=20)privateString poblacion;// ManyToOne dentro de un Embeddable no está soportado en JPA 1.0 (ver en 9.1.34),// pero la implementación de Hibernate lo soporta.
@ManyToOne(fetch=FetchType.LAZY, optional=false) @JoinColumn(name="STATE")private Provincia provincia;publicString getPoblacion(){return poblacion;}publicvoid setPoblacion(String poblacion){this.poblacion = poblacion;}publicString getCalle(){return calle;}publicvoid setCalle(String calle){this.calle = calle;}publicint getCodigoPostal(){return codigoPostal;}publicvoid setCodigoPostal(int codigoPostal){this.codigoPostal = codigoPostal;}public Provincia getProvincia(){return provincia;}publicvoid setProvincia(Provincia provincia){this.provincia = provincia;}}
Como se ve una clase incrustable puede implementar una interfaz (1) y contener referencias (2), entre otras cosas, pero no puede usar métodos de retrollamada de JPA.
Este código se puede usar así, para leer:
Cliente cliente = ...
Direccion direccion = cliente.getDireccion();
direccion.getCalle();// para obtener el valor
O así para establecer una nueva dirección
// para establecer una nueva dirección
Direccion direccion = new Direccion();
direccion.setCalle(“Mi calle”);
direccion.setCodigoPostal(46001);
direccion.setMunicipio(“Valencia”);
direccion.setProvincia(provincia);
cliente.setDireccion(direccion);
En este caso que tenemos una referencia simple, el código generado es un simple JavaBean, cuyo ciclo de vida esta asociado a su objeto contenedor, es decir, la Direccion se borrará y creará junto al Cliente, jamas tendrá vida propia ni podrá ser compartida por otro Cliente.
Colecciones
Colecciones de entidades
Podemos definir colecciones de referencias a entidades. Una colección es una propiedad Java que devuelve java.util.Collection.
Aquí la sintaxis para definir una colección:
@Size (BV, HV, opcional): Cantidad mínima (min) y/o máxima (max) de elementos esperados. Esto se valida antes de grabar.
@Condition (OX, opcional): Para restringir los elementos que aparecen en la colección.
@OrderBy (JPA, opcional): Para que los elementos de la colección aparezcan en un determinado orden.
@XOrderBy (OX, opcional): @OrderBy de JPA no permite usar propiedades calificadas (propiedades de referencias). @XOrderBy sí lo permite.
@OrderColumn (JPA, opcional): (Nuevo en v5.3) El orden de los elementos en la colección se guarda en la base de datos. Una columna especial se crea en la tabla para mantener este orden. La colección ha de ser una java.util.List. La interfaz de usuario permite al usuario reordenar los elementos de la colección.
Declaracion de la colección: Una declaración de colección convencional de Java con sus getters y setters. La colección se marca con @OneToMany (JPA) o @ManyToMany (JPA) y el tipo ha de ser otra entidad.
Vamos a ver algunos ejemplos. Empecemos por uno simple:
Si ponemos esto dentro de una Factura, estamos definiendo una colección de los albaranes asociados a esa Factura. La forma de relacionarlo se hace en la parte del mapeo objeto-relacional. Usamos mappedBy="factura" para indicar que la referencia factura de Albaran se usa para mapear esta colección.
Ahora podemos escribir código como este:
Usar REMOVE como tipo de cascadaas cascade type hace que cuando el usuario borra una factura sus líneas también se borran.
Con @OrderBy obligamos a que las lineas se devuelvan ordenadas por tipoServicio.
La restricción de @Size(min=1) hace que sea obligado que haya al menos una línea para que la factura sea válida.
Tenemos libertad completa para definir como se obtienen los datos de una colección, con @Condition podemos sobreescribir la condición por defecto:
@Condition("${almacen.codigoZona} = ${this.almacen.codigoZona} AND " +
"${almacen.codigo} = ${this.almacen.codigo} AND " +
"NOT (${codigo} = ${this.codigo})")publicCollection<Transportista> getCompañeros(){returnnull;}
Si ponemos esta colección dentro de Transportista, podemos obtener todos los transportista del mismo almacén menos él mismo, es decir, la lista de sus compañeros. Es de notar como podemos usar this en la condición para referenciar al valor de una propiedad del objeto actual. @Condition solo aplica a la interfaz de usuario generada por OpenXava, si llamamos directamente a getFellowCarriers() retornará null.
Si con esto no tenemos suficiente, podemos escribir completamente la lógica que devuelve la colección. La colección anterior también se podría haber definido así:
publicCollection<Transportista> getCompañeros(){Query query = XPersistence.getManager().createQuery("from Transportista t where " +
"t.almacen.codigoZona = :zona AND " +
"t.almacen.codigo = :codigoAlmacen AND " +
"NOT (t.codigo = :codigo) ");
query.setParameter("zona", getAlmacen().getCodigoZona());
query.setParameter("codigoAlmacen", getAlmacen().getCodigo());
query.setParameter("codigo", getCodigo());return query.getResultList();}
Como se ve es un método getter. Obviamente ha de devolver una java.util.Collection cuyos elementos sean de tipo Transportista.
Las referencias de las colecciones se asumen bidireccionales, esto quiere decir que si en un Comercial tengo una colección clientes, en Cliente tengo que tener una referencia a Comercial. Pero puede ocurrir que en Cliente tenga más de una referencia a Comercial (por ejemplo, comercial y comercialAlternativo) y entonce JPA no sabe cual escoger, por eso tenemos el atributo mappedBy de @OneToMany. En este caso pondríamos:
Para indicar que es la referencia comercial y no comercialAlternativo la que vamos a usar para esta colección.
La anotación @ManyToMany (JPA) permite definir una colección con una multiciplidad de muchos-a-muchos. Como sigue:
En este caso un cliente tiene una colección de provincias, pero una misma provincia puede estar presente en varios clientes.
Colecciones incrustadas
Las colecciones de objetos incrustados no se soportaban en las primeras versiones de JPA, por eso con OpenXava las simulabamos usando colecciones a entidades con tipo de cascada REMOVE o ALL. OpenXava trata estas colecciones de una manera especial y seguimos llamando a estas colecciones colecciones incrustadas.
Ahora un ejemplo de una colección incrustada. En la entidad principal (por ejemplo de Factura) podemos poner:
Como se ve esto es una entidad compleja, con calculadores, validadores, referencias y así por el estilo. También hemos de definir una referencia a su clase contenedora (factura). En este caso cuando una factura se borre todas sus líneas se borrarán también. Además hay diferencias a nivel de interface gráfica (podemos aprender más en el capítulo de la vista).
Colecciones de elementos (nuevo en v5.0)
A partir de JPA 2.0 puedes definir una colección de auténticos objetos is. Llamamos a estas colecciones colecciones de elementos.
Esta es la sintaxis para las colecciones de elementos:
@Size (BV, HV, opcional): Cantidad mínima (min) y/o máxima (max) de elementos esperados. Esto se valida antes de grabar.
@OrderBy (JPA, opcional): Para que los elementos de la colección aparezcan en un determinado orden.
@OrderColumn (JPA, opcional): (Nuevo en v5.3) El orden de los elementos en la colección se guarda en la base de datos. Una columna especial se crea en la tabla para mantener este orden. La colección ha de ser una java.util.List. La interfaz de usuario permite al usuario reordenar los elementos de la colección.
Collection declaration: Una declaración de colección Java convencional con sus getters y setters. La colección se marca con @ElementCollection (JPA). Los elementos tienes que ser clases incrustables.
Los elementos en la colección se graban todos a la vez al mismo tiempo que la entidad principal. Además, la interfaz de usuario generada permite modificar todos los elementos de la colección al mismo tiempo.
Una clase incrustable usada en una colección de elementos no puede contener colecciones de ningún tipo.
Veamos un ejemplo. Primero hemos de definir la colección en la entidad principal:
Como se puede ver, una clase incrustable usada en una colección de elementos puede contener referencias(1), validaciones(2) y propiedades calculadas(3) entre otras cosas.
Listas con @OrderColumn (nuevo en v5.3)
Para tener una colección que mantenga el orden de sus elementos se ha de usar java.util.List en lugar de java.util.Collection y hay que anotar la colección con @OrderColumn. Es decir, si definimos una colección de esta forma:
La interfaz de usuario permitirá al usuario cambiar el orden de los elementos y este orden se almacenará en la base de datos. Además, si se cambia el orden de los elementos por código este orden también se persistirá en la base de datos.
Para almacenar el orden, JPA usa una columna especial en la tabla de la base de datos, esta columna es para uso interno exclusivamente y no hay una propiedad para poder acceder a ella desde el código. Podemos usar @OrderColumn(name="MYCOLUMN") para especificar el nombre de la columna si lo necesitamos, si name no se especifica se asume el nombre de la colección más "_ORDER". Si se usa updateSchema, será la herramienta la que cree la columna automáticamente. Si no, es decir, si controlamos el esquema de la base de datos nosotros mismos, deberiamos añadir la columna a la tabla, para la colección de arriba sería así:
ALTERTABLE TAREAPROYECTO
ADD TAREAS_ORDER INTEGER
En la implementación actual el usuario cambia el orden arrastrando y soltando, con colecciones @OneToMany el orden se almacena justo después de soltar, mientras que en las colecciones @ElementCollection el orden se almacen al grabar la entidad contenedora.
Métodos
Los métodos se definen en una entidad OpenXava (mejor dicho, en una entidad JPA) como una clase de Java convencional. Por ejemplo:
Los métodos son la salsa de los objetos, sin ellos solo serían caparazones tontos alrededor de los datos. Cuando sea posible es mejor poner la lógica de negocio en los métodos (capa del modelo) que en las acciones (capa del controlador).
Buscadores
Un buscador es método estático especial que nos permite buscar un objeto o una colección de objetos que sigue algún criterio.
Algunos ejemplos:
publicstatic Cliente findByCodigo(int codigo)throws NoResultException {Query query = XPersistence.getManager().createQuery("from Cliente as o where o.codigo = :codigo");
query.setParameter("codigo", codigo);return(Cliente) query.getSingleResult();}publicstaticCollection findTodos(){Query query = XPersistence.getManager().createQuery("from Cliente as o");return query.getResultList();}publicstaticCollection findByNombreLike(String nombre){Query query = XPersistence.getManager().createQuery("from Cliente as o where o.nombre like :nombre order by o.nombre desc");
query.setParameter("nombre", nombre);return query.getResultList();}
Como se ve, usar método buscadores produce un código más legible que usando la verbosa API de JPA. Pero esto es solo una recomendación de estilo, podemos escoger no escribir métodos buscadores y usar directamente consultas de JPA.
Validador de entidad
Este validador (@EntityValidator) permite poner una validación a nivel de modelo. Cuando necesitamos hacer una validación sobre varias propiedades del modelo, y esta validación no corresponde lógicamente a ninguna de ellas se puede usar este tipo de validación.
Su sintaxis es:
value (opcional, obligada si no se especifica nombre): Clase que implementa la validación. Ha de ser del tipo IValidator.
onlyOnCreate (opcional): Si true el validador es ejecutado solo cuando estamos creando un objeto nuevo, no cuando modificamos uno existente. El valor por defecto es false.
properties (varios @PropertyValue, opcional): Para establecer valor a las propiedades del validador antes de ejecutarse.
Este validador ha de implementar IValidator (1), lo que le obliga a tener un método validate(Messages messages) (2). En este método solo hay que añadir identificadores de mensajes de error (3) (cuyos textos estarán en los archivos i18n), si en el proceso de validación (es decir en la ejecución de todos los validadores) hubiese al menos un mensaje de error, OpenXava no graba la información y visualiza los mensajes al usuario.
En este caso vemos como se accede a descripcion y precioUnitario, por eso la validación se pone a nivel de módelo y no a nivel de propiedad individual, porque abarca más de una propiedad.
A partir de v4.6.1 el validador puede implementar IWithMessage para inyectar el mensaje desde @EntityValidator, funciona como en el caso del validador de propiedad.
Podemos definir más de un validador por entidad usando @EntityValidators, como sigue:
El @RemoveValidator también es un validador a nivel de modelo, la diferencia es que se ejecuta antes de borrar el objeto, y tiene la posibilidad de vetar el borrado.
Su sintaxis es:
packageorg.openxava.test.validadores;importorg.openxava.test.model.*;importorg.openxava.util.*;importorg.openxava.validators.*;/**
* @author Javier Paniza
*/publicclass ValidadorBorrarTipoAlbaran implements IRemoveValidator {// 1private TipoAlbaran tipoAlbaran;privateint codigo;// Usamos esto (en vez de obtenerlo de tipoAlbaran)// para probar @PropertyValue con propiedades simplespublicvoid setEntity(Object entidad)throwsException{// 2this.tipoAlbaran = (TipoAlbaran) entidad;}publicvoid validate(Messages errores)throwsException{if(!tipoAlbaran.getAlbaranes().isEmpty()){
errores.add("no_borrar_tipo_albaran_si_albaranes", newInteger(getCodigo()));// 3}}publicint getCodigo(){return codigo;}publicvoid setCodigo(int codigo){this.codigo = codigo;}}
Como se ve tiene que implementar IRemoveValidator (1) lo que le obliga a tener un método setEntity() (2) con el recibirá el objeto que va a ser borrado. Si hay algún error de validación se añade al objeto de tipo Messages enviado a validate() (3). Si después de ejecutar todas las validaciones OpenXava detecta al menos 1 error de validación no realizará el borrado del objeto y enviará la lista de mensajes al usuario.
En este caso si se comprueba si hay albaranes que usen este tipo de albarán antes de poder borrarlo.
Tal y como ocurre con @EntityValidator podemos usar varios @RemoveValidator por entidad usando la anotación @RemoveValidators. @RemoveValidator se ejecuta cuando borramos entidades desde OpenXava (usando MapFacade o las acciones estándar de OX), pero no cuando usamos directamente JPA. Si queremos crear una restricción al borrar que sea reconocida por JPA, podemos usar un método de retrollamada de JPA, como @PreRemove.
Métodos de retrollamada de JPA
Con @PrePersist podemos indicar que se ejecute cierta lógica justo antes de crear el objeto como persistente.
Como sigue:
En este caso cada vez que se graba por primera vez un TipoAlbaran se añade un sufijo a su descripción.
Como se ve es exactamente igual que cualquier otro método solo que este se ejecuta automáticamente antes de crear.
Con @PreUpdate podemos indicar que se ejecute cierta lógica justo después de modificar un objeto y justo antes de actualizar su contenido en la base de dato, esto es justo antes de hacer el UPDATE.
Como sigue:
En este caso cada vez que se modifica un TipoAlbaran se añade un sufijo a su descripción.
Como se ve es exactamente igual que cualquier otro método solo que este se ejecuta automáticamente antes de modificar.
Podemos usar todas las anotaciones JPA de retrollamada: @PrePersist, @PostPersist, @PreRemove, @PostRemove, @PreUpdate, @PostUpdate y @PostLoad.
Métodos de retrollamada de OX (nuevo en V4.0.1)
Con @PreCreate puede marcar métodos que serán ejecutados antes de persistir algún objeto. De esta manera podrá utilizar el manejador de persistencia o crear busquedas que no son permitidas dentro de las retrollamadas de JPA.
Por ejemplo, si queremos crear un cliente automaticamente si en la factura no se ha seleccionado ninguno.
public onPreCreate {// Crea automaticamente un clienteif(getCliente() == null){
Cliente clte = new Cliente();
clte.setNombre(getNombre());
clte.setDireccion(getDireccion());
clte = XPersistence.getManager().merge(clte);
setCliente(clte);}}
En este ejemplo, la operación del manejador de persistencia, no afectará el comportamiento de este y las demás retrollamadas. Además de @PreCreate están disponible @PostCreate y @PreDelete. Los métodos que son decorados con estas anotaciones forman parte de la misma transacción donde se ejecutaran las retrollamadas de JPA. Cuando estas retrollamadas son combinadas con las de JPA el orden de ejecución es de acuerdo a lo siguiente:
Para crear una entidad: @PreCreate, @PrePersist(JPA), @PostPersist(JPA) y @PostCreate.
Para borrar una entidad: @PreDelete, @PreRemove(JPA) y @PostRemove(JPA).
Los métodos anotados con estas anotaciones deben no retornar ningún valor ni tener ningún parámetro. A diferencia de las retrollamadas de JPA, las de OX sólo funcionan en las entidades mismas y no son tomadas en cuentas en los clases indicadas en @Listeners.
Herencia
OpenXava soporta la herencia de herencia de JPA y Java.
Por ejemplo podemos definer una superclase mapeada (@MappedSuperclass) de esta manera:
packageorg.openxava.test.model;importjavax.persistence.*;importorg.hibernate.annotations.*;importorg.openxava.annotations.*;/**
* Clase base para definir entidades con un oid UUID. <p>
*
* @author Javier Paniza
*/
@MappedSuperclass
publicclass Identificable {
@Id @GeneratedValue(generator="system-uuid") @Hidden
@GenericGenerator(name="system-uuid", strategy = "uuid")privateString oid;publicString getOid(){return oid;}publicvoid setOid(String oid){this.oid = oid;}}
Podemos definir otra @MappedSuperclass que extienda de esta, por ejemplo:
packageorg.openxava.test.model;importjavax.persistence.*;importorg.openxava.annotations.*;/**
* Clase base para entidades con una propiedad 'nombre'. <p>
*
* @author Javier Paniza
*/
@MappedSuperclass
publicclass ConNombre extends Identifiable {
@Column(length=50) @Required
privateString nombre;publicString getNombre(){return nombre;}publicvoid setNombre(String nombre){this.nombre = nombre;}}
Ahora podemos usar Identificable y ConNombre para definir nuestra entidades, como sigue:
packageorg.openxava.test.model;importjavax.persistence.*;/**
*
* @author Javier Paniza
*/
@Entity
@DiscriminatorColumn(name="TYPE")
@DiscriminatorValue("HUM")
@Table(name="PERSONA")
@AttributeOverrides(
@AttributeOverride(name="name", column=@Column(name="PNOMBRE")))publicclass Humano extends ConNombre {
@Enumerated(EnumType.STRING)private Sexo sexo;publicenum Sexo { MASCULINO, FEMENINO };public Sexo getSexo(){return sexo;}publicvoid setSexo(Sexo sexo){this.sexo = sexo;}}
Y ahora, la auténtica herencia de entidades, una entidad que extiende de otra entidad:
Podemo crear un módulo OpenXava para Humano y Programador (no para Identificable ni ConNombre directamente). En el módulo de Programador el usuario puede acceder solo a programadores, por otra parte usando el módulo de Humano el usuario puede acceder a objetos de tipo Humano y Programador. Además cuando el usuario trata de visualizar el detalle de un Programador desde el módulo de Humano se mostrará la vista de Programador. Polimorfismo puro.
A partir de v4.5 OpenXava soporta todas las características de la herencia de JPA, incluyendo una única tabla por jerarquica, tabla para superclase y tabla para subclase (joined) y tabla por clase como estrategias de mapeo, antes de v4.5 sólo @AttributeOverrides y la estrategia de una única tabla por jerarquía se soportaban.
Clave múltiple
La forma preferida para definir la clave de una entidad es una clave única autogenerada (anotada con @Id y @GeneratedValue), pero a veces, por ejemplo cuando vamos contra bases de datos legadas, necesitamos tener una entidad mapeada a una tabla que usa varias columnas como clave. Este caso se pude resolver con JPA (y por tanto con OpenXava) de dos formas, usando @IdClass o usando @EmbeddedId
Clase id
En este caso usamos @IdClass en nuestra entidad para indicar una clase clave, y marcamos las propiedades clave como @Id en nuestra entidad:
packageorg.openxava.test.model;importjavax.persistence.*;importorg.openxava.annotations.*;importorg.openxava.jpa.*;/**
*
* @author Javier Paniza
*/
@Entity
@IdClass(AlmacenKey.class)publicclass Almacen {
@Id
// Column también se especifica en AlmacenKey por un bug en Hibernate, ver// http://opensource.atlassian.com/projects/hibernate/browse/ANN-361
@Column(length=3, name="ZONA")privateint codigoZona;
@Id @Column(length=3)privateint codigo;
@Column(length=40) @Required
privateString nombre;publicString getNombre(){return nombre;}publicvoid setNombre(String nombre){this.nombre = nombre;}publicint getCodigo(){return codigo;}publicvoid setCodigo(int codigo){this.codigo = codigo;}publicint getCodigoZona(){return codigoZona;}publicvoid setCodigoZona(int codigoZona){this.codigoZona = codigoZona;}}
También necesitamos declarar una clase id, una clase serializable normal y corriente con todas las propiedades clave de la entidad:
OpenXava tiene soporte completo del estándar Java para validación: Bean Validation. (1.1 JSR-349 desde v5.3 y 1.0 JSR-303 desde v4.1) Podemos definir nuestras propias restricciones en nuestras entidades como se explica en la especificación Bean Validation, y OpenXava las reconocerá, mostrando los mensajes de error correspondientes al usuario. Consulta la última documentación de Hibernate Validator para aprender como escribir un validador JSR-349, ya que la versión actual de Hibernate Validator implementa JSR-349.
Además, a partir de v5.3 las anotaciones de OpenXava @Required, @PropertyValidator y @EntityValidator están definidas como restricciones de Bean Validation, esto significa que cuando grabamos una entidad usando directamente JPA estas validaciones se aplicarán.
Por otra parte, @RemoveValidator, @PropertyValidator(onlyOnCreate=true), EntityValidator(onlyOnCreate=true) y la característica de validador por defecto de OpenXava no son reconocidas ni por Bean Validation ni por JPA, sino solo por OpenXava.
@AssertTrue
A partir de v4.9 OpenXava permite inyectar propiedades y propiedades calificadas (propiedades de referencias) del bean validado, en el mensaje identificado mediante el elemento message de AssertTrue. Por ejemplo:
En este caso tenemos a @AssertTrue anotando el campo de la Entidad:
{no_puede_circular} es el identificador de mensaje que se encuentra declarado en el archivo i18n así:
no_puede_circular={tipo} de placa {placa} no es apto para circular. No se puede asignar al conductor {conductor.nombre}
Si tenemos la entidad con: tipo=AUTO, placa=A1-0001 y puedeCircular=false; e intentamos asignar conductor (nombre=MIGUEL GRAU), el método de validación fallará y se mostrará el mensaje de error:
AUTO de placa A1-0001 no es apto para circular. No se puede asignar al conductor MIGUEL GRAU
Hibernate Validator (nuevo en v3.0.1)
OpenXava tiene soporte completo de Hibernate Validator con soporte de Bean Validation (nuevo en v4.1). Hibernate Validator 3.x (con la vieja API) se soportó hasta v5.2.x. Podemos definir nuestras propias restricciones en nuestras entidades como se explica en la documentación de Hibernate Validator, y OpenXava las reconocerá, mostrando los mensajes de error correspondientes al usuario.
Además, las anotaciones de OpenXava @Required, @PropertyValidator y @EntityValidator están definidas como restricciones de Hibernate Validator 3.x hasta v5.2.x y como restricciones de Bean Validation a partir de v5.3, esto significa que cuando grabamos una entidad usando directamente JPA estas validaciones se aplicarán.
Table of Contents
Modelo
La capa del modelo en una aplicación orientada a objetos es la que contiene la lógica de negocio, esto es la estructura de los datos con los que se trabaja y todos los cálculos, validaciones y procesos asociados a esos datos.OpenXava es un marco orientado al modelo, en donde el modelo es lo más importante, y todo lo demás (p. ej. la interfaz gráfica) depende de él.
La forma de definir el modelo en OpenXava es mediante simples clases Java (aunque también existe una versión XML) y un poquito de Java. OpenXava provee una aplicación completamente funcional a partir de la definición del modelo.
Componente de negocio
La unidad básica para crear aplicaciones OpenXava es el componente de negocio. Un componente de negocio se define usando una clase Java llamada Entity. Esta clase es una entidad EJB3 convencional, o con otras palabras, un POJO con anotaciones que sigue el estándar Java Persistence API (JPA).JPA es el estándar de Java para la persistencia, es decir, para objetos que guardan su estado en una base de datos. Si sabes desarrollar usando POJOs con JPA, ya sabes como desarrollar aplicaciones OpenXava.
Usando una simple clase Java podemos definir un Componente de Negocio con:
- Modelo: Estrutura de datos, validaciones, calculos, etc.
- Vista: Cómo se puede mostrar el modelo al usuario.
- Datos tabulares: Cómo se muestra los datos de este componentes en modo lista (en formato tabular).
- Mapeo objeto/relacional: Cómo grabar y leer el estado de los objetos desde la base de datos.
Este capítulo explica cómo definir la parte del modelo, es decir, todo sobre la estructura, las validaciones, los cálculos, etc.Entidad
Para definir la parte del modelo hemos de crear una clase Java o Groovy (desde v4m6) con anotaciones. Además de sus propias anotaciones, OpenXava sporta anotaciones de JPA, Hibernate Validator e Hibernate Annotations. Esta clase Java es una entidad, es decir, una clase persistente que representa concepto de negocio.En este capítulo JPA se usa para indicar que es una anotación estándar de Java Persistent API, HV para indicar que es una anotación de Hibernate Validator, HA para indicar que es una anotación de Hibernate Annotations y OX para indicar que es una anotación de OpenXava.
Ésta es la sintáxis para una entidad:
Clases incrustables (Embeddable)
Tal y como indica la especificación JPA:"Una entidad puede usar otras clases finamente granuladas para representar su estado. Instancias de estas clases, no como en el caso de las entidades, no tiene identidad persistente. En vez de eso, existen solo como objetos incrustados de una entidad a la que pertenecen. Estos objetos incrustados son propiedad exclusiva de sus entidades dueñas, y no se comparten entre entidades persistentes."
La sintaxis para una clase incrustada es:
Propiedades
Una propiedad representa parte del estado de un objeto que se puede consultar y en algunos casos cambiar. El objeto no tiene la obligación de guardar físicamente la información de la propiedad, solo de devolverla cuando se le pregunte.La sintaxis para definir una propiedad es:
Estereotipo
Un estereotipo (@Stereotype) es la forma de determinar un comportamiento especifico dentro de un tipo. Por ejemplo, un nombre, un comentario, una descripción, etc. todos corresponden al tipo Java java.lang.String pero si queremos que los validadores, logitud por defecto, editores visuales, etc. sean diferente en cada caso y necesitamos afinar más; lo podemos hacer asignando un esterotipo a cada uno de estos casos. Es decir, podemos tener los estereotipos NOMBRE, TEXTO_GRANDE o DESCRIPCION y asignarlos a nuestras propiedades.El OpenXava viene configurado con los siguientes estereotipos:
- DINERO, MONEY
- FOTO, PHOTO, IMAGEN, IMAGE
- TEXTO_GRANDE, MEMO, TEXT_AREA
- ETIQUETA, LABEL
- ETIQUETA_NEGRITA, BOLD_LABEL
- HORA, TIME
- FECHAHORA, DATETIME
- GALERIA_IMAGENES, IMAGES_GALLERY (instrucciones)
- RELLENADO_CON_CEROS, ZEROS_FILLED
- TEXTO_HTML, HTML_TEXT (texto con formato editable)
- TEXTO_HTML_SIMPLE, SIMPLE_HTML_TEXT (nuevo en v5.6, como TEXT_HTML pero con una interfaz simplificada)
- ETIQUETA_IMAGEN, IMAGE_LABEL (imagen que depende del contenido de la propiedad)
- EMAIL
- TELEFONO, TELEPHONE
- WEBURL
- IP
- ISBN
- TARJETA_CREDITO, CREDIT_CARD
- LISTA_EMAIL, EMAIL_LIST
- LIBRERIA_DOCUMENTOS, DOCUMENT_LIBRARY (nuevo en v4m6, sólo funciona dentro de Liferay)
- CONTRASENA, PASSWORD (nuevo en v4.1)
- MAC (nuevo en v4.8)
- ARCHIVO, FILE (nuevo en v5.0) (instrucciones)
- ARCHIVOS, FILES (nuevo en v5.1) (instrucciones)
- ICONO, ICON (nuevo en v5.5)
- DISCUSION, DISCUSSION (nuevo en v5.6) (instrucciones)
Vamos a ver como definiríamos un estereotipo propio. Crearemos uno llamado NOMBRE_PERSONA para representar nombres de persona.Editamos (o creamos) el archivo editors.xml o editores.xml en nuestra carpeta xava. Y añadimos
De esta forma indicamos que editor se ha de ejecutar para editar y visualizar propiedades con el estereotipo NOMBRE_PERSONA.
Además es útil indicar la longitud por defecto, eso se hace editando default-size.xml o longitud-defecto.xml:
<para-estereotipo nombre="NOMBRE_PERSONA" longitud="40"/>Y así si no ponemos longitud asumirá 40 por defecto.Menos común es querer cambiar el validador para requerido, pero si queremos cambiarlo lo podemos hacer añadiendo a validators.xml o validadores.xml de nuestro proyecto lo siguiente:
Ahora podemos definir propiedades con estereotipo NOMBRE_PERSONA:
En este caso asume 40 longitud y tipo String, así como ejecutar el validador NotBlankCharacterValidator
para comprobar que es requerido.
Estereotipo GALERIA_IMAGENES
Si queremos que una propiedad de nuestro componente almacene una galería de imágenes. Solo necesitamos declarar que nuestra propiedad sea del estereotipo GALERIA_IMAGENES. De esta manera:Además, en el mapeo tenemos que mapear la propiedad contra una columna adecuada para almacenar una cadena (String) con 32 caracteres de longitud (VARCHAR(32)).
Y ya está todo.
Pero, para que nuestra aplicación soporte este estereotipo necesitamos configurar nuestro sistema.
Lo primero es crear a tabla en la base de datos para almacenar las imágenes:
El tipo de la columna IMAGE puede ser un tipo más adecuado para almacenar byte [] en el caso de nuestra base de datos (por ejemplo LONGVARBINARY) .
Y finalmente necesitamos definir el mapeo en nuestro archivo persistence/hibernate.cfg.xml, así:
Después de todo esto ya podemos usar el estereotipo GALERIA_IMAGENES en los componentes de nuestra aplicación.
Estereotipos ARCHIVO (nuevo en v5.0) y ARCHIVOS (nuevo en v5.1)
Si queremos que una propiedad de nuestro componente adjunte un archivo, sólo necesitamos declarar una propiedad con el estereotipo ARCHIVO. De esta manera:Para adjuntar múltiples archivos:
OpenXava puede almacenar los archivos en base de datos o en el sistema de archivos.
Almacenamiento en el Sistema de Archivos
Éste es por defecto y no requiere de configuraciones adicionales.El directorio de almacenamiento es $HOME/oxfiles (GNU/Linux) o %USERPROFILE%\oxfiles (Windows). Podemos configurar un directorio diferente mediante la propiedad filesPath en xava.properties.
Almacenamiento en Base de Datos
Debemos realizar tres configuraciones adicionales:- Modificar el persistence.xml de nuestro proyecto.
Observa que hemos añadido <class>org.openxava.web.editors.AttachedFile</class> a ambas unidades de persistencia.- Al generar nuestra base de datos se creará la tabla OXFILES:
Debemos verificar que el tipo de la columna DATA sea el tipo más adecuado para almacenar byte[] (en nuestro caso LONGVARBINARY).Estereotipo DISCUSION (nuevo en v5.6)
Si quieres que una propiedad de tu componente contenga un hilo de discusión, como los que hay en los foros, gestores de incidencias, blogs, etc. Sólo has de declarar tu propiedad con el estereotipo DISCUSION, de esta manera:Si además quieres que cuando se borre tu entidad su discusión también se elimine completamente de la base de datos, has de añadir el siguiente código a tu entidad:
Verifica que persistence.xml contiene la entidad DiscussionComment, si no añádelo:
Fíjate que hemos añadido <class>org.openxava.web.editors.DiscussionComment</class>. Cuando se genere la base datos, la tabla OXDISCUSSIONCOMMENTS se creará:
Comprueba que el tipo para la columna COMMENT es el más adecuado para almacenar un texto grande (CLOB por defecto) en tu base de datos, si no haz un ALTER COLUMN para poner un tipo mejor.
Con Tomcat 6 has de editar la clase org.openxava.web.editors.DiscussionComment y quitarle la anotación @Lob de la propiedad comment.
Concurrencia y propiedad versión
Concurrencia es la habilidad de una aplicación para permitir que varios usuarios graben datos al mismo tiempo sin perder información. OpenXava usa un esquema de concurrencia optimista. Usando concurrencia optimista los registros no se bloquean permitiendo un alto nivel de concurrencia sin perder la integridad de la información.Por ejemplo, si un usuario A lee un registro y entonces un usuario B lee el mismo registro, lo modifica y graba los cambios, cuando el usuario A intente grabar el registro recibirá un error y tendrá que refrescar los datos y reintentar su modificación.
Para activar el soporte de concurrencia para un componente OpenXava solo necesitamos declarar una propiedad usando @Version, de esta manera:
Esta propiedad es para uso del mecanismo de persistencia (Hibernate o JPA), ni nuestra aplicación ni usuarios deberían acceder directamente a ella.
Enums
OpenXava soporta enums de Java 5. Un enum permite definir una propiedad que solo puede contener los valores indicados.Es fácil de usar, veamos un ejemplo:
La propiedad distancia solo puede valer LOCAL, NACIONAL o INTERNACIONAL, y como no hemos puesto @Required también permite valor vacío (null). Desde v5.3 si pones @Required, la primera opción es por defecto y ya no mostrará valor vacío. Si deseas cambiar la opción por defecto usa @DefaultValueCalculator. Desde v5.6.1 los enums anotados con @Required en una clase incrustable mostrarán valor vacío si ésta es utilizada en una colección de elementos.
A nivel de interfaz gráfico la implementación web actual usa un combo. La etiqueta para cada valor se obtienen de los archivos i18n.
A nivel de base datos por defecto guarda el entero (0 para LOCAL, 1 para NACIONAL, 2 para INTERNACIONAL y null para cuando no hay valor), pero esto se puede configurar fácilmente para poder usar sin problemas bases de datos legadas. Ver más de esto último en el capítulo sobre mapeo.
Propiedades calculadas
Las propiedades calculadas son de solo lectura (solo tienen getter) y no persistentes (no se almacenan en ninguna columna de la tabla de base de datos).Una propiedad calculada se define de esta manera:
De acuerdo con esta definición ahora podemos usar el código de esta manera:
Y resultado contendrá 332,772.
Cuando la propiedad precioUnitarioEnPesetas se visualiza al usuario no es editable, y su editor tiene una longitud de 10, indicado usando @Max(9999999999L) (2). También, dado que usamos @Depends("precioUnitario") (1) cuando el usuario cambie la propiedad precioUnitario en la interfaz de usuario la propiedad precioUnitarioEnPesetas será recalculada y su valor será refrescado de cara al usuario.
Desde una propiedad calculada tenemos acceso a conexiones JDBC. Un ejemplo:
Es verdad, el código JDBC es feo y complicado, pero a veces puede ayudar a resolver problemas de rendimiento. La clase DataSourceConnectionProvider nos permite obtener la conexión asociada a la misma fuente de datos que la entidad indicada (en este caso Factura). Esta clase es para nuestra conveniencia, también podemos acceder a una conexión JDBC usando JNDI o cualquier otro medio que queramos. De hecho, en una propiedad calculada podemos escribir cualquier código que Java nos permita.
Si estamos usando acceso basado en propiedades, es decir si anotamos los getters o setters, entonces hemos de añadir la anotación @Transient a nuestra propiedad calculada, de esta forma:
Formula (nuevo en v3.1.4)
Usando @Formula de Hibernate Annotations podemos definir un cálculo para nuestra propiedad. Este cálculo se expresa usando SQL, y es ejecutado en la propia base de datos, en vez de por Java. Simplemente hemos de escribir un fragmento válido de SQL:El uso es simple. Hemos de poner el cálculo como lo hariamos si lo tuvieramos que poner en una sentencia SQL.
Normalmente las propiedades con @Formula son propiedades de solo lectura, es decir, solo tienen getter, no tienen setter. Cuando el objeto es leído de la base de datos se hace el cálculo por la misma base de datos y se rellena la propiedad con el resultado.
Esto es una alternativa a las propiedades calculadas. Tiene la ventaja de que el usuario puede filtrar por esta propiedad en modo lista, y la desventaja de que hemos de usar SQL en vez de Java, y no podemos usar @Depends para recalcular el valor en vivo.
Calculador valor por defecto
Con @DefaultValueCalculator podemos asociar lógica a una propiedad, en este caso la propiedad es lectura y escritura. Este calculador se usa para calcular el valor inicial. Por ejemplo:En este caso cuando el usuario intenta crear una nueva factura (por ejemplo) se encontrará con que el campo de año ya tiene valor, que él puede cambiar si quiere. La lógica para generar este valor está en la clase CurrentYearCalculator class, así:
Es posible personalizar el comportamiento de un calculador poniendo el valor de sus propiedades, como sigue:
En este caso para calcular el valor por defecto OpenXava instancia StringCalculator y entonces inyecta el valor "BUENA" en la propiedad string de StringCalculator, y finalmente llama al método calculate() para obtener el valor por defecto para relacionConComercial. Como se ve, el uso de la anotación @PropertyValue permite crear calculadores reutilizable.
@PropertyValue permite inyectar valores desde otras propiedades visualizadas, de esta forma:
En este caso antes de ejecutar el calculador OpenXava llena la propiedad permisoConducir de CalculadorObservacionesTransportista con el valor de la propiedad visualizada tipo de la referencia permisoConducir. Como se ve el atributo from soporta propiedades calificadas (referencia.propiedad). Además, cada ve que permisoConducir.tipo cambia observaciones se recalcula (nuevo en v5.1, con versiones anteriores se recalculaba solo la primera vez).
Además podemos usar @PropertyValue sin from ni value:
En este caso OpenXava coge el valor de la propiedad visualizada codigoFamilia y lo inyecta en la propiedad codigoFamilia del calculador, es decir @PropertyValue(name="codigoFamilia") equivale a @PropertyValue(name="codigoFamilia", from="codigoFamilia").
Desde un calculador tenemos acceso a conexiones JDBC, he aquí un ejemplo:
Y la clase del calculador:
Para usar JDBC nuestro calculador tiene que implementar IJDBCCalculator (1) y entonces recibirá un IConnectionProvider (2) que podemos usar dentro de calculate().
OpenXava dispone de un conjunto de calculadores incluidos de uso genérico, que se pueden encontrar en org.openxava.calculators.
Valores por defecto al crear
Podemos indicar que el valor sea calculado justo antes de crear (insertar en la base de datos) un objeto por primera vez.Usualmente para las claves usamos el estándar JPA. Por ejemplo, si queremos usar una columna identity (auto incremento) como clave:
Podemos usar otras técnicas de generación, por ejemplo, una sequence de base de datos puede ser definida usando el estándar JPA de esta manera:
Si queremos generar un identificador único de tipo String y 32 caracteres, podemos usar una extensión de Hibernate de JPA:
Ver la sección 9.1.9 de la especificación JPA 1.0 (parte de JSR-220) para aprender más sobre @GeneratedValues.
Si queremos usar nuestra propia lógica para generar el valor al crear, o bien queremos generar un nuevo valor para propiedades que no son clave entonces no podemos usar el @GeneratedValue de JPA, aunque es fácil resolver estos casos con JPA. Solo necesitamos añadir este código a nuestra clase:
La anotación JPA @PrePersist hace que este método se ejecute antes de insertar datos por primera vez en la base de datos, en este método podemos calcular el valor para nuestra clave o incluso para propiedades no clave con nuestra propia lógica.
Validador de propiedad
Un validador de propiedad (@PropertyValidator) ejecuta la lógica de validación sobre el valor que se vaya a asignar a esa propiedad antes de grabar. Una propiedad puede tener varios validadores:La forma de configurar el validador (con los @PropertyValue, aunque el atributo from no funciona, hay que usar value siempre) es exactamente igual como en los calculadores. Con el atributo onlyOnCreate=”true” se puede definir que esa validación solo se ejecute cuando se crea el objeto, y no cuando se modifica.
El código del validador es:
Un validador ha de implementar IPropertyValidator (1), esto le obliga a tener un método validate() en donde se ejecuta la validación de la propiedad. Los argumentos del método validate() son:
- Messages errores: Un objeto de tipo Messages que representa un conjunto de mensajes (una especie de colección inteligente) y es donde podemos añadir los problemas de validación que encontremos.
- Object valor: El valor a validar.
- String nombreObjeto: Nombre del objeto al que pertenece la propiedad a validar. Útil para usarlo en los mensajes de error.
- String nombrePropiedad: Nombre de la propiedad a validar. Útil para usarlo en los mensajes de error.
Como se ve cuando encontramos un error de validación solo tenemos que añadirlo (con errores.add()) enviando un identificador de mensaje y los argumentos. Para que este validador produzca un mensaje significativo tenemos que tener en nuestro archivo de mensajes i18n la siguiente entrada:excluir_cadena={0} no puede contener {2} en {1}Si el identificador que se envía no está en el archivo de mensajes, sale tal cual al usuario; pero lo recomendado es siempre usar identificadores del archivo de mensajes.La validación es satisfactoria si no se añaden mensajes y se supone fallida si se añaden. El sistema recolecta todos los mensajes de todos los validadores antes de grabar y si encuentra los visualiza al usuario y no graba.
A partir de v4.6.1 también es posible usar en el validador el mensaje de @PropertyValidator. Es decir, podemos escribir:
Si el mensaje está entre llaves se obtiene de los archivos i18n, si no se usa tal cual.
Además, hemos de implementar la interfaz IWithMessage en el validador:
El mensaje especificado en la anotación @PropertyValidator, libro_rpg_no_permitido, se inyecta en el validador llamando a setMessage(). Este mensaje puede ser añadido directamente como un error.
El paquete org.openxava.validators contiene algunos validadores de uso común.
@PropertyValidator está definida como una restriccion de Bean Validation a partir de v5.3 y como una restricción de Hibernate Validator hasta v5.2.x.
Si necesitas usar JPA en tu validador, mira Usar JPA en un validador o método de retrollamada.
Validador por defecto (nuevo en v2.0.3)
Podemos definir validadores por defecto para las propiedades de cierto tipo o estereotipo. Para esto se usa el archivo xava/validadores.xml de nuestro proyecto para definir en él los validadores por defecto.Por ejemplo, podemos definir en nuestro xava/validadores.xml lo siguiente:
En este caso estamos asociando el validador ValidadorNombrePersona al estereotipo NOMBRE_PERSONA. Ahora si definimos una propiedad como la siguiente:
Esta propiedad será validada usando ValidadorNombrePersona aunque la propiedad misma no defina ningun validador. ValidadorNombrePersona se aplica a todas las propiedades con el estereotipo NOMBRE_PERSONA.
Podemos también asignar validadores por defecto a un tipo.
En el archivo validadores.xml podemos definir también los validadores para determinar si un valor requerido está presente (ejecutado cuando usamos @Required). Además podemos asignar nombre (alias) a las clases de los validadores.
Podemos aprender más sobre los validadores examinando OpenXava/xava/default-validators.xml y OpenXavaTest/xava/validators.xml.
Los validadores por defecto no se aplican cuando grabamos nuestras entidades directamente con la api de JPA.
Cálculo (nuevo en v5.7)
Con @Calculation podemos definir una expresión aritmética para hacer el cálculo de la propiedad. La expresión puede contener +, -, *, /, (), valores numéricos y nombres de propiedades de la misma entidad. Por ejemplo:Fíjate como trabajador.precioHora se usa para obtener el valor de una referencia.
El cálculo se ejecuta y visualiza cuando el usuario cambia cualquier valor de las propiedades usadas en la expresión en la interfaz de usuario, sin embargo el valor no se graba hasta que el usuario no pulsa en el botón de grabar.
Referencias
Una referencia hace que desde una entidad o agregado se pueda acceder otra entidad o agregado. Una referencia se traduce a código Java como una propiedad (con su getter y su setter) cuyo tipo es el del modelo al que se referencia. Por ejemplo un Cliente puede tener una referencia a su Comercial, y así podemos escribir código Java como éste:para acceder al nombre del comercial de ese cliente.
La sintaxis para definir referencias es:
- @ManyToOne(optional=false) (JPA), @Required (OX) (opcional, el JPA es el preferido): Indica si la referencia es requerida. Al grabar OpenXava comprobará si las referencias requeridas están presentes, si no lo están no se producirá la grabación y se devolverá una lista de errores de validación.
- @Id (JPA, opcional): Para indicar si la referencia forma parte de la clave. La combinación de propiedades y referencias clave se debe mapear a un conjunto de campos en la base de datos que no tengan valores repetidos, típicamente con la clave primaria.
- @DefaultValueCalculator (OX, one, opcional): Para implementar la lógica para calcular el valor inicial de la referencia. Este calculador ha de devolver el valor de la clave, que puede ser un dato simple (solo si la clave del objeto referenciado es simple) o un objeto clave (un objeto especial que envuelve la clave primaria).
- @SearchKey (OX, optional): (Nuevo en v3.0.2) Las referencias clave de búsqueda se usan por los usuarios para buscar los objetos. Son editables en la interfaz de usuario de las referencias permitiendo al usuario teclear su valor para buscar. OpenXava usa los miembros clave (@Id) para buscar por defecto, y si los miembros clave (@Id) están ocultos usa la primera propiedad en la vista. Con @SearchKey podemos elegir referencias para buscar explícitamente.
- Declaración de la referencia: Una declaración de referencia convencional de Java con sus getters y setters. La referencia se marca con @ManyToOne (JPA) y el tipo ha de ser otra entidad.
Un pequeño ejemplo de referencias:- Una referencia llamada comercial a la entidad Comercial.
- Una referencia llamada comercialAlternativo a la entidad Comercial. En este caso usamos fetch=FetchType.LAZY, de esta manera los datos son leidos de la base de datos bajo demanda. Este es el enfoque más eficiente, pero no es el valor por defecto en JPA, por tanto es aconsejable usar siempre fetch=FetchType.LAZY al declarar las referencias.
Si asumimos que esto está en una entidad llamada Cliente, podemos escribir:Calculador valor por defecto en referencias
En una referencia @DefaultValueCalculator funciona como en una propiedad, solo que hay que devolver el valor de la clave de la referencia.Por ejemplo, en el caso de una referencia con clave simple podemos poner:
El método calculate() de este calculador es:
Como se puede ver se devuelve un entero, es decir, el valor para familia por defecto es la familia cuyo código es el 2.
En el caso de clave compuesta sería así:
Y el código del calculador:
Devuelve un objeto de tipo Almacen pero rellenando sólo las propiedades clave.
Usar referencias como clave
Podemos usar referencias como clave, o como parte de la clave. Hemos de declarar la referencia como@Id, y usar una clase clave, como sigue:
Además, necesitamos escribir la clase clave:
Necesitamos escribir la clase clave aunque la clave sea solo una referencia con una sola columna clave.
Es mejor usar esta característica sólo cuando estemos trabajando contra bases de datos legadas, si tenemos control sobre el esquema es mejor usar un id autogenerado.
Referencias incrustadas
Podemos referenciar a una clase incrustable usando la anotación @Embedded. Por ejemplo, en la entidad principal podemos escribir:Y hemos de definir la clase Direccion como incrustable:
Como se ve una clase incrustable puede implementar una interfaz (1) y contener referencias (2), entre otras cosas, pero no puede usar métodos de retrollamada de JPA.
Este código se puede usar así, para leer:
O así para establecer una nueva dirección
En este caso que tenemos una referencia simple, el código generado es un simple JavaBean, cuyo ciclo de vida esta asociado a su objeto contenedor, es decir, la Direccion se borrará y creará junto al Cliente, jamas tendrá vida propia ni podrá ser compartida por otro Cliente.
Colecciones
Colecciones de entidades
Podemos definir colecciones de referencias a entidades. Una colección es una propiedad Java que devuelve java.util.Collection.Aquí la sintaxis para definir una colección:
- @Size (BV, HV, opcional): Cantidad mínima (min) y/o máxima (max) de elementos esperados. Esto se valida antes de grabar.
- @Condition (OX, opcional): Para restringir los elementos que aparecen en la colección.
- @OrderBy (JPA, opcional): Para que los elementos de la colección aparezcan en un determinado orden.
- @XOrderBy (OX, opcional): @OrderBy de JPA no permite usar propiedades calificadas (propiedades de referencias). @XOrderBy sí lo permite.
- @OrderColumn (JPA, opcional): (Nuevo en v5.3) El orden de los elementos en la colección se guarda en la base de datos. Una columna especial se crea en la tabla para mantener este orden. La colección ha de ser una java.util.List. La interfaz de usuario permite al usuario reordenar los elementos de la colección.
- Declaracion de la colección: Una declaración de colección convencional de Java con sus getters y setters. La colección se marca con @OneToMany (JPA) o @ManyToMany (JPA) y el tipo ha de ser otra entidad.
Vamos a ver algunos ejemplos. Empecemos por uno simple:Si ponemos esto dentro de una Factura, estamos definiendo una colección de los albaranes asociados a esa Factura. La forma de relacionarlo se hace en la parte del mapeo objeto-relacional. Usamos mappedBy="factura" para indicar que la referencia factura de Albaran se usa para mapear esta colección.
Ahora podemos escribir código como este:
Para hacer algo con todos los albaranes asociados a una factura.
Vamos a ver otro ejemplo más complejo, también dentro de Factura:
- Usar REMOVE como tipo de cascadaas cascade type hace que cuando el usuario borra una factura sus líneas también se borran.
- Con @OrderBy obligamos a que las lineas se devuelvan ordenadas por tipoServicio.
- La restricción de @Size(min=1) hace que sea obligado que haya al menos una línea para que la factura sea válida.
Tenemos libertad completa para definir como se obtienen los datos de una colección, con @Condition podemos sobreescribir la condición por defecto:Si ponemos esta colección dentro de Transportista, podemos obtener todos los transportista del mismo almacén menos él mismo, es decir, la lista de sus compañeros. Es de notar como podemos usar this en la condición para referenciar al valor de una propiedad del objeto actual. @Condition solo aplica a la interfaz de usuario generada por OpenXava, si llamamos directamente a getFellowCarriers() retornará null.
Si con esto no tenemos suficiente, podemos escribir completamente la lógica que devuelve la colección. La colección anterior también se podría haber definido así:
Como se ve es un método getter. Obviamente ha de devolver una java.util.Collection cuyos elementos sean de tipo Transportista.
Las referencias de las colecciones se asumen bidireccionales, esto quiere decir que si en un Comercial tengo una colección clientes, en Cliente tengo que tener una referencia a Comercial. Pero puede ocurrir que en Cliente tenga más de una referencia a Comercial (por ejemplo, comercial y comercialAlternativo) y entonce JPA no sabe cual escoger, por eso tenemos el atributo mappedBy de @OneToMany. En este caso pondríamos:
Para indicar que es la referencia comercial y no comercialAlternativo la que vamos a usar para esta colección.
La anotación @ManyToMany (JPA) permite definir una colección con una multiciplidad de muchos-a-muchos. Como sigue:
En este caso un cliente tiene una colección de provincias, pero una misma provincia puede estar presente en varios clientes.
Colecciones incrustadas
Las colecciones de objetos incrustados no se soportaban en las primeras versiones de JPA, por eso con OpenXava las simulabamos usando colecciones a entidades con tipo de cascada REMOVE o ALL. OpenXava trata estas colecciones de una manera especial y seguimos llamando a estas colecciones colecciones incrustadas.Ahora un ejemplo de una colección incrustada. En la entidad principal (por ejemplo de Factura) podemos poner:
Es de notar que usamos CascadeType.REMOVE y LineaFactura es una entidad y no una clase incrustable:
Como se ve esto es una entidad compleja, con calculadores, validadores, referencias y así por el estilo. También hemos de definir una referencia a su clase contenedora (factura). En este caso cuando una factura se borre todas sus líneas se borrarán también. Además hay diferencias a nivel de interface gráfica (podemos aprender más en el capítulo de la vista).
Colecciones de elementos (nuevo en v5.0)
A partir de JPA 2.0 puedes definir una colección de auténticos objetos is. Llamamos a estas colecciones colecciones de elementos.Esta es la sintaxis para las colecciones de elementos:
- @Size (BV, HV, opcional): Cantidad mínima (min) y/o máxima (max) de elementos esperados. Esto se valida antes de grabar.
- @OrderBy (JPA, opcional): Para que los elementos de la colección aparezcan en un determinado orden.
- @OrderColumn (JPA, opcional): (Nuevo en v5.3) El orden de los elementos en la colección se guarda en la base de datos. Una columna especial se crea en la tabla para mantener este orden. La colección ha de ser una java.util.List. La interfaz de usuario permite al usuario reordenar los elementos de la colección.
- Collection declaration: Una declaración de colección Java convencional con sus getters y setters. La colección se marca con @ElementCollection (JPA). Los elementos tienes que ser clases incrustables.
Los elementos en la colección se graban todos a la vez al mismo tiempo que la entidad principal. Además, la interfaz de usuario generada permite modificar todos los elementos de la colección al mismo tiempo.Una clase incrustable usada en una colección de elementos no puede contener colecciones de ningún tipo.
Veamos un ejemplo. Primero hemos de definir la colección en la entidad principal:
Ahora definimos nuestra clase incrustada:
Como se puede ver, una clase incrustable usada en una colección de elementos puede contener referencias(1), validaciones(2) y propiedades calculadas(3) entre otras cosas.
Listas con @OrderColumn (nuevo en v5.3)
Para tener una colección que mantenga el orden de sus elementos se ha de usar java.util.List en lugar de java.util.Collection y hay que anotar la colección con @OrderColumn. Es decir, si definimos una colección de esta forma:La interfaz de usuario permitirá al usuario cambiar el orden de los elementos y este orden se almacenará en la base de datos. Además, si se cambia el orden de los elementos por código este orden también se persistirá en la base de datos.
Para almacenar el orden, JPA usa una columna especial en la tabla de la base de datos, esta columna es para uso interno exclusivamente y no hay una propiedad para poder acceder a ella desde el código. Podemos usar @OrderColumn(name="MYCOLUMN") para especificar el nombre de la columna si lo necesitamos, si name no se especifica se asume el nombre de la colección más "_ORDER". Si se usa updateSchema, será la herramienta la que cree la columna automáticamente. Si no, es decir, si controlamos el esquema de la base de datos nosotros mismos, deberiamos añadir la columna a la tabla, para la colección de arriba sería así:
En la implementación actual el usuario cambia el orden arrastrando y soltando, con colecciones @OneToMany el orden se almacena justo después de soltar, mientras que en las colecciones @ElementCollection el orden se almacen al grabar la entidad contenedora.
Métodos
Los métodos se definen en una entidad OpenXava (mejor dicho, en una entidad JPA) como una clase de Java convencional. Por ejemplo:Los métodos son la salsa de los objetos, sin ellos solo serían caparazones tontos alrededor de los datos. Cuando sea posible es mejor poner la lógica de negocio en los métodos (capa del modelo) que en las acciones (capa del controlador).
Buscadores
Un buscador es método estático especial que nos permite buscar un objeto o una colección de objetos que sigue algún criterio.Algunos ejemplos:
Estos métodos se pueden usar de esta manera:
Como se ve, usar método buscadores produce un código más legible que usando la verbosa API de JPA. Pero esto es solo una recomendación de estilo, podemos escoger no escribir métodos buscadores y usar directamente consultas de JPA.
Validador de entidad
Este validador (@EntityValidator) permite poner una validación a nivel de modelo. Cuando necesitamos hacer una validación sobre varias propiedades del modelo, y esta validación no corresponde lógicamente a ninguna de ellas se puede usar este tipo de validación.Su sintaxis es:
- value (opcional, obligada si no se especifica nombre): Clase que implementa la validación. Ha de ser del tipo IValidator.
- onlyOnCreate (opcional): Si true el validador es ejecutado solo cuando estamos creando un objeto nuevo, no cuando modificamos uno existente. El valor por defecto es false.
- properties (varios @PropertyValue, opcional): Para establecer valor a las propiedades del validador antes de ejecutarse.
Un ejemplo:Y el código del validador:
Este validador ha de implementar IValidator (1), lo que le obliga a tener un método validate(Messages messages) (2). En este método solo hay que añadir identificadores de mensajes de error (3) (cuyos textos estarán en los archivos i18n), si en el proceso de validación (es decir en la ejecución de todos los validadores) hubiese al menos un mensaje de error, OpenXava no graba la información y visualiza los mensajes al usuario.
En este caso vemos como se accede a descripcion y precioUnitario, por eso la validación se pone a nivel de módelo y no a nivel de propiedad individual, porque abarca más de una propiedad.
A partir de v4.6.1 el validador puede implementar IWithMessage para inyectar el mensaje desde @EntityValidator, funciona como en el caso del validador de propiedad.
Podemos definir más de un validador por entidad usando @EntityValidators, como sigue:
@EntityValidator está definida como una restriccion de Bean Validation a partir de v5.3 y como una restricción de Hibernate Validator hasta v5.2.x.
Si necesitas usar JPA en tu validador, mira Usar JPA en un validador o método de retrollamada.
Validador al borrar
El @RemoveValidator también es un validador a nivel de modelo, la diferencia es que se ejecuta antes de borrar el objeto, y tiene la posibilidad de vetar el borrado.Su sintaxis es:
- clase (obligada): Clase que implementa la validación. Ha de ser del tipo IRemoveValidator.
- properties (varios @PropertyValue, opcional): Para establecer valor a las propiedades del calculador antes de ejecutarse.
Un ejemplo puede ser:Y el validador:
Como se ve tiene que implementar IRemoveValidator (1) lo que le obliga a tener un método setEntity() (2) con el recibirá el objeto que va a ser borrado. Si hay algún error de validación se añade al objeto de tipo Messages enviado a validate() (3). Si después de ejecutar todas las validaciones OpenXava detecta al menos 1 error de validación no realizará el borrado del objeto y enviará la lista de mensajes al usuario.
En este caso si se comprueba si hay albaranes que usen este tipo de albarán antes de poder borrarlo.
Tal y como ocurre con @EntityValidator podemos usar varios @RemoveValidator por entidad usando la anotación @RemoveValidators.
@RemoveValidator se ejecuta cuando borramos entidades desde OpenXava (usando MapFacade o las acciones estándar de OX), pero no cuando usamos directamente JPA. Si queremos crear una restricción al borrar que sea reconocida por JPA, podemos usar un método de retrollamada de JPA, como @PreRemove.
Métodos de retrollamada de JPA
Con @PrePersist podemos indicar que se ejecute cierta lógica justo antes de crear el objeto como persistente.Como sigue:
En este caso cada vez que se graba por primera vez un TipoAlbaran se añade un sufijo a su descripción.
Como se ve es exactamente igual que cualquier otro método solo que este se ejecuta automáticamente antes de crear.
Con @PreUpdate podemos indicar que se ejecute cierta lógica justo después de modificar un objeto y justo antes de actualizar su contenido en la base de dato, esto es justo antes de hacer el UPDATE.
Como sigue:
En este caso cada vez que se modifica un TipoAlbaran se añade un sufijo a su descripción.
Como se ve es exactamente igual que cualquier otro método solo que este se ejecuta automáticamente antes de modificar.
Podemos usar todas las anotaciones JPA de retrollamada: @PrePersist, @PostPersist, @PreRemove, @PostRemove, @PreUpdate, @PostUpdate y @PostLoad.
Métodos de retrollamada de OX (nuevo en V4.0.1)
Con @PreCreate puede marcar métodos que serán ejecutados antes de persistir algún objeto. De esta manera podrá utilizar el manejador de persistencia o crear busquedas que no son permitidas dentro de las retrollamadas de JPA.Por ejemplo, si queremos crear un cliente automaticamente si en la factura no se ha seleccionado ninguno.
En este ejemplo, la operación del manejador de persistencia, no afectará el comportamiento de este y las demás retrollamadas. Además de @PreCreate están disponible @PostCreate y @PreDelete. Los métodos que son decorados con estas anotaciones forman parte de la misma transacción donde se ejecutaran las retrollamadas de JPA. Cuando estas retrollamadas son combinadas con las de JPA el orden de ejecución es de acuerdo a lo siguiente:
Para crear una entidad: @PreCreate, @PrePersist(JPA), @PostPersist(JPA) y @PostCreate.
Para borrar una entidad: @PreDelete, @PreRemove(JPA) y @PostRemove(JPA).
Los métodos anotados con estas anotaciones deben no retornar ningún valor ni tener ningún parámetro. A diferencia de las retrollamadas de JPA, las de OX sólo funcionan en las entidades mismas y no son tomadas en cuentas en los clases indicadas en @Listeners.
Herencia
OpenXava soporta la herencia de herencia de JPA y Java.Por ejemplo podemos definer una superclase mapeada (@MappedSuperclass) de esta manera:
Podemos definir otra @MappedSuperclass que extienda de esta, por ejemplo:
Ahora podemos usar Identificable y ConNombre para definir nuestra entidades, como sigue:
Y ahora, la auténtica herencia de entidades, una entidad que extiende de otra entidad:
Podemo crear un módulo OpenXava para Humano y Programador (no para Identificable ni ConNombre directamente). En el módulo de Programador el usuario puede acceder solo a programadores, por otra parte usando el módulo de Humano el usuario puede acceder a objetos de tipo Humano y Programador. Además cuando el usuario trata de visualizar el detalle de un Programador desde el módulo de Humano se mostrará la vista de Programador. Polimorfismo puro.
A partir de v4.5 OpenXava soporta todas las características de la herencia de JPA, incluyendo una única tabla por jerarquica, tabla para superclase y tabla para subclase (joined) y tabla por clase como estrategias de mapeo, antes de v4.5 sólo @AttributeOverrides y la estrategia de una única tabla por jerarquía se soportaban.
Clave múltiple
La forma preferida para definir la clave de una entidad es una clave única autogenerada (anotada con @Id y @GeneratedValue), pero a veces, por ejemplo cuando vamos contra bases de datos legadas, necesitamos tener una entidad mapeada a una tabla que usa varias columnas como clave. Este caso se pude resolver con JPA (y por tanto con OpenXava) de dos formas, usando @IdClass o usando @EmbeddedIdClase id
En este caso usamos @IdClass en nuestra entidad para indicar una clase clave, y marcamos las propiedades clave como @Id en nuestra entidad:También necesitamos declarar una clase id, una clase serializable normal y corriente con todas las propiedades clave de la entidad:
Id inscrustado
En este case tenemos una referencia a un objeto incrustado (@Embeddable) marcada como @EmbeddedId:Y nuestra clave es una clase incrustable que contiene las propiedades clave:
Bean Validation
OpenXava tiene soporte completo del estándar Java para validación: Bean Validation. (1.1 JSR-349 desde v5.3 y 1.0 JSR-303 desde v4.1) Podemos definir nuestras propias restricciones en nuestras entidades como se explica en la especificación Bean Validation, y OpenXava las reconocerá, mostrando los mensajes de error correspondientes al usuario. Consulta la última documentación de Hibernate Validator para aprender como escribir un validador JSR-349, ya que la versión actual de Hibernate Validator implementa JSR-349.Además, a partir de v5.3 las anotaciones de OpenXava @Required, @PropertyValidator y @EntityValidator están definidas como restricciones de Bean Validation, esto significa que cuando grabamos una entidad usando directamente JPA estas validaciones se aplicarán.
Por otra parte, @RemoveValidator, @PropertyValidator(onlyOnCreate=true), EntityValidator(onlyOnCreate=true) y la característica de validador por defecto de OpenXava no son reconocidas ni por Bean Validation ni por JPA, sino solo por OpenXava.
@AssertTrue
A partir de v4.9 OpenXava permite inyectar propiedades y propiedades calificadas (propiedades de referencias) del bean validado, en el mensaje identificado mediante el elemento message de AssertTrue. Por ejemplo:En este caso tenemos a @AssertTrue anotando el campo de la Entidad:
{no_puede_conducir} es el identificador de mensaje que se encuentra declarado en el archivo i18n así:
no_puede_conducir=Conductor {nombre} no puede ser registrado: debe aprobar el examen de conducirSi intentamos crear una entidad con nombre=MIGUEL GRAU y puedeConducir=false se mostrará el mensaje de error:Conductor MIGUEL GRAU no puede ser registrado: debe aprobar el examen de conducir
En este caso tenemos a @AssertTrue anotando un método de la Entidad:
{no_puede_circular} es el identificador de mensaje que se encuentra declarado en el archivo i18n así:
no_puede_circular={tipo} de placa {placa} no es apto para circular. No se puede asignar al conductor {conductor.nombre}Si tenemos la entidad con: tipo=AUTO, placa=A1-0001 y puedeCircular=false; e intentamos asignar conductor (nombre=MIGUEL GRAU), el método de validación fallará y se mostrará el mensaje de error:AUTO de placa A1-0001 no es apto para circular. No se puede asignar al conductor MIGUEL GRAU
Hibernate Validator (nuevo en v3.0.1)
OpenXava tiene soporte completo de Hibernate Validator con soporte de Bean Validation (nuevo en v4.1). Hibernate Validator 3.x (con la vieja API) se soportó hasta v5.2.x. Podemos definir nuestras propias restricciones en nuestras entidades como se explica en la documentación de Hibernate Validator, y OpenXava las reconocerá, mostrando los mensajes de error correspondientes al usuario.Además, las anotaciones de OpenXava @Required, @PropertyValidator y @EntityValidator están definidas como restricciones de Hibernate Validator 3.x hasta v5.2.x y como restricciones de Bean Validation a partir de v5.3, esto significa que cuando grabamos una entidad usando directamente JPA estas validaciones se aplicarán.